# -*- coding: utf-8 -*-

import re
import os
import traceback
from conf.entity.module.grpc_config import ProtoConfig
from com.maycur.grpc.base.grpc_entity import *
from com.maycur.utils import CommonUtils

# global variables
proto_syntax = 'syntax = "proto3";\n\n'
proto_empty = 'google.protobuf.Empty'
proto_empty_package = 'google/protobuf/empty.proto'

# boolean types
has_empty = False
# list types
grpc_import_list = []


class IceAndProtoConverter(object):
    config: GrpcConfig
    proto_config: ProtoConfig
    mapping_config: GrpcMappingConfig
    mapping: GrpcFullMapping
    module_leading_methods: List[str]
    ice_2_proto_type_dict: Dict[str, str]
    result: bool

    def __init__(self, config: GrpcConfig, mapping: GrpcFullMapping):
        self.config = config
        self.mapping = mapping
        self.proto_config = config.proto_config
        self.mapping_config = config.mapping_config
        self.result = True
        self.module_leading_methods = CommonUtils.lower_list_as_set(self.proto_config.module_leading_methods)
        self.ice_2_proto_type_dict = self.mapping.ice.ice_2_proto_type_dict

    def leading_with_message_prefix(self, name: str) -> str:
        message_prefix = self.proto_config.message_prefix
        if message_prefix is not None or message_prefix != '':
            name = message_prefix + name
        return name

    def parse_ice_file(self, ice_f):
        enum_start = False
        message_start = False
        service_start = False
        field_number = 1
        enum_field_number = 0
        grpc_enum = None
        grpc_message = None
        grpc_service = None
        last_comment_line = None
        for ice_line in ice_f:
            # filter message field like 'sequenceNo'
            if self.is_target_struct_line(ice_line, 'sequence') and '<' in ice_line:
                self.parse_ice_sequence_type(ice_line)
            elif self.is_target_struct_line(ice_line, 'dictionary'):
                self.parse_ice_dictionary_type(ice_line)
            elif self.is_target_struct_line(ice_line, 'enum'):
                enum_start = True
                origin_enum_name = self.parse_ice_struct_name(ice_line)
                # enum_name optimize
                enum_name = self.trim_ice_name(origin_enum_name)
                grpc_enum = GrpcEnum(enum_name, origin_enum_name)
                grpc_enum.source = ProtoSource.ICE
                grpc_enum.leading_prefix = self.config.proto_config.message_prefix
                self.ice_2_proto_type_dict[origin_enum_name] = grpc_enum.get_optimized_name()
            elif self.is_target_struct_line(ice_line, 'class') and ';' in ice_line:
                # skip definition line such as 'class MIceSubsidiaryTreeNode;'
                continue
            elif self.is_target_struct_line(ice_line, 'class', 'struct'):
                message_start = True
                origin_class_name = self.parse_ice_struct_name(ice_line)
                # class_name optimize
                class_name = self.trim_ice_name(origin_class_name)
                grpc_message = GrpcMessage(class_name, origin_class_name)
                grpc_message.source = ProtoSource.ICE
                grpc_message.leading_prefix = self.config.proto_config.message_prefix
                self.ice_2_proto_type_dict[origin_class_name] = grpc_message.get_optimized_name()
            elif self.is_target_struct_line(ice_line, 'interface'):
                service_start = True
                grpc_service = self.parse_ice_service(ice_line)
            elif enum_start:
                if '}' not in ice_line:
                    enum_fields_pair = self.parse_ice_enum_field(ice_line, enum_field_number)
                    if len(enum_fields_pair[0]) > 0:
                        for item in enum_fields_pair[0]:
                            grpc_enum.add_field(item)
                    enum_field_number = enum_fields_pair[1]
                else:
                    self.collect_grpc_struct(grpc_enum)
                    self.collect_unresolved_sequence_type(grpc_enum)
                    enum_start = False
                    enum_field_number = 0
            elif message_start:
                if '}' not in ice_line:
                    ice_line = re.sub(r'\s+', ' ', ice_line)
                    if ';' not in ice_line:
                        last_comment_line = self.parse_comment(ice_line)
                        continue
                    grpc_field = self.parse_message_field(ice_line, field_number, last_comment_line)
                    grpc_message.add_field(grpc_field)
                    field_number += 1
                    last_comment_line = None
                else:
                    self.collect_grpc_struct(grpc_message)
                    self.collect_unresolved_sequence_type(grpc_message)
                    message_start = False
                    field_number = 1
            elif service_start and not CommonUtils.is_empty_line(ice_line):
                if '}' not in ice_line:
                    if ';' not in ice_line:
                        last_comment_line = self.parse_comment(ice_line)
                        continue
                    grpc_method = self.parse_ice_method(ice_line, grpc_service)
                    if grpc_method is not None:
                        # handle comment
                        grpc_method.comment = self.parse_comment(ice_line) or last_comment_line
                        grpc_service.add_method(grpc_method)
                    last_comment_line = None
                else:
                    self.mapping.ice.ice_2_grpc_services.append(grpc_service)
                    service_start = False

    def grpc_struct_exists(self, struct: GrpcBaseStruct) -> bool:
        return struct.get_optimized_name() in self.mapping.grpc.struct_dict

    def collect_grpc_struct(self, struct: GrpcBaseStruct):
        self.mapping.grpc.struct_dict[struct.get_optimized_name()] = struct

    def collect_unresolved_sequence_type(self, struct: GrpcBaseStruct):
        if struct.source_type in self.mapping.ice.sequence_unresolved_dict:
            ice_sequence_type = self.mapping.ice.sequence_unresolved_dict[struct.source_type]
            self.ice_2_proto_type_dict[ice_sequence_type] = f'repeated {struct.get_optimized_name()}'
            del self.mapping.ice.sequence_unresolved_dict[struct.source_type]

    def parse_ice_service(self, ice_service_line: str):
        ice_service_name = self.parse_ice_struct_name(ice_service_line)
        grpc_service_name = ice_service_name.replace('Servant', '')
        if not grpc_service_name.endswith(self.proto_config.service_suffix):
            grpc_service_name += self.proto_config.service_suffix

        grpc_service = GrpcService(grpc_service_name, self.config)
        grpc_service.source = ProtoSource.ICE
        return grpc_service

    def parse_ice_sequence_type(self, ice_sequence_line: str) -> None:
        try:
            # in case of ["java:type:java.util.ArrayList<String>"] at the beginning
            ice_sequence_line = ice_sequence_line[ice_sequence_line.index('sequence'):]
            sequence_line = ice_sequence_line.replace(';', '')
            sequence_line = sequence_line.replace('sequence', '').strip()
            sequence_split = re.sub(r'\s+', ' ', sequence_line).split(' ')
            ice_type = sequence_split[0].replace('<', '').replace('>', '')
            self.mapping.ice.sequence_dict[sequence_split[1]] = ice_type
            if ice_type in self.ice_2_proto_type_dict:
                self.ice_2_proto_type_dict[sequence_split[1]] = f'repeated {self.ice_2_proto_type_dict[ice_type]}'
            else:
                self.mapping.ice.sequence_unresolved_dict[ice_type] = sequence_split[1]
        except Exception as e:
            print(f'parse ice sequence failed, the line is: {ice_sequence_line}')
            print(f'exception detail: {e}')

    @staticmethod
    def parse_ice_enum_field(ice_enum_f_line: str, f_number: int) -> (List[GrpcEnumField], int):
        f_line = re.sub(r'\s+', ' ', ice_enum_f_line.strip())
        parts = f_line.split(',')

        res = []
        for part in parts:
            strip_part = part.strip()
            if strip_part != '':
                strip_part = strip_part.split('=')[0].strip()
                enum_field = GrpcEnumField(strip_part, f_number)
                res.append(enum_field)
                f_number = f_number + 1
        return res, f_number

    def parse_message_field(self, ice_line: str, f_number: int, last_comment_line: str) -> GrpcField:
        field_split = ice_line.split(';')[0].split(' ')
        option = None
        if len(field_split) == 4:
            # it seems that proto3 removed 'required' and 'optional'
            # https://stackoverflow.com/questions/31801257/why-required-and-optional-is-removed-in-protocol-buffers-3
            # option = field_split[1]
            f_type = field_split[2]
            f_name = field_split[3]
        else:
            f_type = field_split[1]
            # trim optional again
            if 'optional' in f_type:
                f_type = f_type[f_type.index(')') + 1:]
            f_name = field_split[2]

        # field_name optimize
        f_name = self.trim_ice_name(f_name)

        # type redefine
        f_type_resolved = False
        if f_type in self.ice_2_proto_type_dict:
            f_type = self.ice_2_proto_type_dict[f_type]
            f_type_resolved = True

        grpc_field = GrpcField(option, f_type, f_name, f_number)
        grpc_field.set_f_type_resolved(f_type_resolved)

        # handle comment
        grpc_field.comment = self.parse_comment(ice_line) or last_comment_line

        return grpc_field

    def parse_ice_method(self, ice_method_line: str, grpc_service: GrpcService):
        global has_empty

        def leading_with_service_module(message: GrpcBaseStruct, the_lower_method: str, tag: str):
            if the_lower_method in self.module_leading_methods or self.grpc_struct_exists(message):
                module_name = grpc_service.module_name
                if grpc_service.name in self.mapping_config.ice_2_grpc:
                    module_name = CommonUtils.upper_first(self.mapping_config.ice_2_grpc[grpc_service.name].module)
                print(f'the {tag}', message.name,
                      f'already exists or is ambiguous, add leading service module: {module_name}')
                message.name = module_name + message.name
                the_optimized_name = module_name + message.name
                message.optimized_name = self.leading_with_message_prefix(the_optimized_name)

        method_line = ice_method_line.strip()
        method_line = re.sub(r'\s+', ' ', method_line)
        name_and_response = method_line[:method_line.index('(')].replace('idempotent', '').strip()
        name_and_response_split = name_and_response.split(' ')
        ice_request = method_line[method_line.index('('):]
        method_name = name_and_response_split[1]
        lower_method = method_name.lower()
        ice_response_type = name_and_response_split[0]

        if f'{grpc_service.name}@{method_name}' in self.mapping_config.ice_2_grpc:
            # the method already defined in .proto, skip parsing
            return

        grpc_request = CommonUtils.upper_first(method_name) + self.proto_config.request_suffix
        request_message = GrpcMessage(grpc_request, method_name)
        request_message.leading_prefix = self.config.proto_config.message_prefix
        ice_request = ice_request.replace('(', '')
        ice_request = ice_request.split(')')[0]
        request_split = ice_request.split(',')
        for idx, item in enumerate(request_split):
            if item.strip() == '':
                # no request param
                continue
            pair = item.strip().split(' ')
            ice_type = pair[0]
            f_type = self.ice_2_proto_type_dict[ice_type]
            f_type = self.get_grpc_wrapped_map_type(ice_type, f_type)
            if ice_type not in self.mapping.ice.basic_ice_2_proto_type_dict:
                grpc_service.add_related_struct(f_type, self.mapping)
            request_field = GrpcField(None, f_type, pair[1], idx + 1)
            request_message.add_field(request_field)
        leading_with_service_module(request_message, lower_method, 'request')
        grpc_service.add_related_struct(request_message, self.mapping)
        self.collect_grpc_struct(request_message)

        grpc_method = GrpcMethod(method_name)
        if 'void' == ice_response_type:
            grpc_method.response = proto_empty
            has_empty = True
        else:
            grpc_response_type = self.ice_2_proto_type_dict[ice_response_type]
            if ice_response_type not in self.mapping.ice.basic_ice_2_proto_type_dict:
                grpc_service.add_related_struct(grpc_response_type, self.mapping)
            response_name = CommonUtils.upper_first(method_name) + self.proto_config.response_suffix
            f_name = self.get_optimized_response_field_name(ice_response_type)
            grpc_response_type = self.get_grpc_wrapped_map_type(ice_response_type, grpc_response_type)
            response_field = GrpcField(None, grpc_response_type, f_name, 1)
            response_message = GrpcMessage(response_name, method_name)
            response_message.leading_prefix = self.config.proto_config.message_prefix
            response_message.add_field(response_field)

            leading_with_service_module(response_message, lower_method, 'response')
            grpc_service.add_related_struct(response_message, self.mapping)
            grpc_method.response = response_message.get_optimized_name()
            self.collect_grpc_struct(response_message)
        grpc_method.request = request_message.get_optimized_name()
        return grpc_method

    def parse_ice_dictionary_type(self, ice_dictionary_line: str) -> None:
        ice_dictionary_line = ice_dictionary_line[ice_dictionary_line.index('dictionary'):]
        dictionary_line = ice_dictionary_line.replace(';', '')
        dictionary_line = dictionary_line.replace('dictionary', '')
        # <string, int> demoMap === <string,int>demoMap === <string,int | demoMap
        dictionary_split = re.sub(r'\s+', '', dictionary_line).split('>')
        if len(dictionary_split) == 2:
            dictionary_key = dictionary_split[1]
            dictionary_values = dictionary_split[0].replace('<', '').split(',')
            self.mapping.ice.dictionary_dict[dictionary_key] = dictionary_values
            grpc_map_key = self.ice_2_proto_type_dict[dictionary_values[0]]
            grpc_map_value = self.ice_2_proto_type_dict[dictionary_values[1]]
            grpc_map_type = f'map<{grpc_map_key}, {grpc_map_value}>'
            self.ice_2_proto_type_dict[dictionary_key] = grpc_map_type
            self.mapping.grpc.map_dict[grpc_map_type] = [grpc_map_key, grpc_map_value]
        else:
            raise RuntimeError(f'complicated ice dictionary: {dictionary_line} not supported')

    def get_optimized_response_field_name(self, ice_response_type: str) -> str:
        field_type = self.ice_2_proto_type_dict[ice_response_type]
        full_field_type = field_type
        field_type = field_type.replace('repeated', '').strip()
        if 'bool' == field_type:
            return 'success'
        elif 'int32' == field_type or 'int64' == field_type or 'string' == field_type:
            return 'res'
        elif 'map<' in field_type:
            return CommonUtils.lower_first(self.trim_ice_name(ice_response_type))
        elif 'repeated' in full_field_type:
            return CommonUtils.plural(CommonUtils.lower_first(field_type))
        return CommonUtils.lower_first(field_type)

    def get_grpc_wrapped_map_type(self, ice_type: str, grpc_type) -> str:
        # process ice dictionary with sequence
        if 'map<' in grpc_type and 'repeated' in grpc_type:
            holder_name = CommonUtils.lower_first(self.trim_ice_name(ice_type))
            if holder_name.lower().endswith('map'):
                holder_name = holder_name[:-3]
            holder_message_name = CommonUtils.upper_first(holder_name) + 'Holder'
            if self.leading_with_message_prefix(holder_message_name) not in self.mapping.grpc.struct_dict:
                # create sequence holder message in map
                holder_message = GrpcMessage(holder_message_name, ice_type)
                holder_message.leading_prefix = self.proto_config.message_prefix
                holder_f_type = self.mapping.grpc.map_dict[grpc_type][1]
                holder_field = GrpcField(None, holder_f_type, holder_name, 1)
                holder_message.add_field(holder_field)

                self.collect_grpc_struct(holder_message)

            holder_message = self.mapping.grpc.struct_dict[self.leading_with_message_prefix(holder_message_name)]
            # map<string, repeated GrpcDepartment> -> map<string, GrpcDepartmentHolder>
            grpc_type_core = holder_message.get_optimized_name()
            repeated_idx = grpc_type.index('repeated ')
            last_bracket_idx = grpc_type.rindex('>')
            grpc_type = grpc_type[:repeated_idx] + grpc_type_core + grpc_type[last_bracket_idx:]
            self.mapping.grpc.map_dict[grpc_type] = holder_message
        return grpc_type

    def do_convert_ice_to_proto(self, ice_file: str, grpc_file: str):
        try:
            with open(ice_file, 'r') as ice_f:
                self.parse_ice_file(ice_f)
        except Exception as e:
            self.result = False
            print(f'Failed to parse ice file: {self.config.ice_config.file_name}, error message: {e}.')
            print("Details are as follows: ")
            traceback.print_exc()
        else:
            if self.config.test_mode:
                return

            # write to target proto file
            if not os.path.exists(grpc_file):
                with open(grpc_file, 'x') as grpc_f:
                    self.do_save_to_proto(grpc_f)
            else:
                self.do_update_to_proto(grpc_file)

    @staticmethod
    def trim_ice_name(name):
        if 'MIce' in name:
            name = name.replace('MIce', '')
        return name

    def is_target_struct_line(self, line: str, *struct_keywords: str) -> bool:
        """
        ignore comment line, if any keyword in line, it's the start of the target struct
        """
        if self.is_comment_line(line):
            return False
        for keyword in struct_keywords:
            if keyword in line:
                return True
        return False

    @staticmethod
    def is_comment_line(line: str) -> bool:
        return '//' in line

    def parse_comment(self, ice_line: str) -> str:
        if CommonUtils.is_empty_line(ice_line):
            return ''
        if self.is_comment_line(ice_line):
            ice_line = ice_line.replace('\n', '')
            ice_line = ice_line[ice_line.index('//'):]
            if ice_line[2] != ' ':
                ice_line = '// ' + ice_line[2:]
            return ice_line
        return ''

    @staticmethod
    def parse_ice_struct_name(ice_line: str) -> str:
        line_split = re.sub(r'\s+', ' ', ice_line).split(' ')
        origin_struct_name = line_split[2]
        # in case of no blank in front of '{'
        return origin_struct_name.replace('{', '')

    @staticmethod
    def parse_grpc_struct_name(grpc_line: str) -> str:
        line_split = re.sub(r'\s+', ' ', grpc_line).split(' ')
        origin_struct_name = line_split[1]
        # in case of no blank in front of '{'
        return origin_struct_name.replace('{', '')

    def do_save_to_proto(self, grpc_f):
        grpc_f.write(proto_syntax)
        grpc_f.write(f'package {self.proto_config.package};\n')
        grpc_f.write(f'option java_package = "{self.proto_config.java_package}";\n')
        if self.proto_config.enable_multiple_files:
            grpc_f.write('option java_multiple_files = true;\n')
        grpc_f.write('\n')

        # imports
        if has_empty:
            grpc_f.write(f'import "{proto_empty_package}";\n')
        if len(grpc_import_list) > 0:
            grpc_f.writelines(grpc_import_list)
        grpc_f.write('\n')

        # message & services
        if len(self.mapping.ice.ice_2_grpc_services) > 0:
            for item in self.mapping.ice.ice_2_grpc_services:
                grpc_f.write(item.build(self.mapping))

    def do_update_to_proto(self, exist_grpc_f):
        with open(exist_grpc_f, 'r') as exist_f:
            lines = exist_f.readlines()
            first_message_idx = -1
            message_name_set = set()
            method_name_set = set()
            service_name_dict = {}
            service_name = None
            service_start = False
            service_first_idx = -1
            for idx, line in enumerate(lines):
                if first_message_idx < 0 and 'message' in line:
                    first_message_idx = idx
                if self.is_target_struct_line(line, 'message '):
                    message_name = self.parse_grpc_struct_name(line)
                    message_name_set.add(message_name)
                elif self.is_target_struct_line(line, 'enum '):
                    message_name = self.parse_grpc_struct_name(line)
                    message_name_set.add(message_name)
                elif self.is_target_struct_line(line, 'service '):
                    service_start = True
                    service_name = self.parse_grpc_struct_name(line)
                    service_first_idx = idx
                elif self.is_target_struct_line(line, 'rpc '):
                    method_name = line[:line.index('(')].replace('rpc ', '').strip()
                    method_name_set.add(method_name)
                elif service_start:
                    if '}' in line:
                        service_name_dict[service_name] = (service_first_idx, idx)
                        service_start = False
            # message & services
            if len(self.mapping.ice.ice_2_grpc_services) > 0:
                for item in self.mapping.ice.ice_2_grpc_services:
                    item.update_build(message_name_set, method_name_set, lines, service_name_dict, self.mapping)

        # update to proto
        with open(exist_grpc_f, 'w') as exist_f:
            exist_f.writelines(lines)
            print('update success')

    def convert_ice_to_proto(self):
        ice_config = self.config.ice_config
        grpc_config = self.config.proto_config
        ice_root = f'{ice_config.project_path}/{ice_config.root_path}'
        proto_root = f'{grpc_config.project_path}/{grpc_config.root_path}'
        ice_file = f'{ice_root}/{ice_config.file_name}'
        proto_file = f'{proto_root}/{grpc_config.file_name}'
        ice_file = re.sub('/+', '/', ice_file)
        proto_file = re.sub('/+', '/', proto_file)

        self.do_convert_ice_to_proto(ice_file, proto_file)
